---
title: Extending Admin Navigation
sidebarTitle: Navigation
---

Spree Admin Dashboard provides a flexible navigation system that allows you to easily extend the sidebar navigation with your own menu items without modifying the core codebase. This enables safe updates while maintaining your customizations.

<Info>
  Starting with Spree 5.2, the navigation system uses a declarative API accessible via `Spree.admin.navigation`. This allows you to programmatically add, modify, and remove navigation items directly in your initializers.
</Info>

## Basic Usage

Add navigation items in your `config/initializers/spree.rb` file:

```ruby config/initializers/spree.rb
Rails.application.config.after_initialize do
  sidebar_nav = Spree.admin.navigation.sidebar

  sidebar_nav.add :brands,
    label: :brands,
    url: :admin_brands_path,
    icon: 'award',
    position: 35,
    active: -> { controller_name == 'brands' },
    if: -> { can?(:manage, Spree::Brand) }
end
```

## Available Options

All navigation items support the following options:

<ParamField path="label" type="Symbol or String" required>
  The text label for the navigation item. Can be a symbol (translation key using `Spree.t`) or a string.
</ParamField>

<ParamField path="url" type="Symbol or Lambda" required>
  The URL for the navigation item. Can be a route helper symbol (`:admin_brands_path`) or a lambda returning a URL.
</ParamField>

<ParamField path="icon" type="String">
  Icon name from [Tabler Icons](https://tabler.io/icons).
</ParamField>

<ParamField path="position" type="Integer" default="0">
  Numeric position in the menu. Lower numbers appear first.
</ParamField>

<ParamField path="active" type="Lambda">
  Lambda to determine if the link should be highlighted as active. Receives view context.
</ParamField>

<ParamField path="if" type="Lambda">
  Conditional display logic. The item only appears if this lambda returns true. Has access to view context helpers like `can?`, `current_store`, etc.
</ParamField>

<ParamField path="badge" type="String or Lambda">
  Badge text/count to display next to the label. Can be a string or lambda that returns a value.
</ParamField>

<ParamField path="badge_class" type="String">
  CSS class for badge styling (e.g., `'badge-info'`, `'badge-warning'`).
</ParamField>

<ParamField path="tooltip" type="String">
  Tooltip text shown on hover.
</ParamField>

<ParamField path="target" type="String">
  Link target attribute (e.g., `'_blank'` to open in a new tab).
</ParamField>

<ParamField path="section_label" type="String">
  Creates a section divider with the given label instead of a clickable link.
</ParamField>

<ParamField path="parent" type="Symbol">
  The key of an existing navigation item to nest this item under. This is the simplest way to add items to existing submenus.
</ParamField>

## Navigation Contexts

The navigation system supports multiple contexts. Spree provides predefined contexts for common use cases, and you can register custom contexts for your specific needs.

### Sidebar Navigation

The main sidebar navigation:

```ruby
sidebar_nav = Spree.admin.navigation.sidebar
```

### Settings Navigation

Navigation in the Settings area:

```ruby
settings_nav = Spree.admin.navigation.settings
```

### Page Tab Navigation

Spree provides several predefined tab contexts for common admin pages:

```ruby
tax_tabs = Spree.admin.navigation.tax_tabs
shipping_tabs = Spree.admin.navigation.shipping_tabs
team_tabs = Spree.admin.navigation.team_tabs
stock_tabs = Spree.admin.navigation.stock_tabs
returns_tabs = Spree.admin.navigation.returns_tabs
developers_tabs = Spree.admin.navigation.developers_tabs
audit_tabs = Spree.admin.navigation.audit_tabs
```

### Registering Contexts

Use `register_context` to create a new navigation context:

```ruby
# Returns a Spree::Admin::Navigation instance
custom_tabs = Spree.admin.navigation.register_context(:custom_tabs)
```

<ParamField path="name" type="Symbol or String" required>
  The unique name for the navigation context. Will be converted to a symbol internally.
</ParamField>

**Returns:** `Spree::Admin::Navigation` - The navigation context instance

**Note:** Calling `register_context` multiple times with the same name returns the same instance (idempotent).

### Custom Tab Contexts

You can create custom tab contexts for your own admin pages using `register_context`:

```ruby config/initializers/spree.rb
Rails.application.config.after_initialize do
  # Register custom tab navigation for your brands page
  brand_tabs = Spree.admin.navigation.register_context(:brand_tabs)

  brand_tabs.add :active_brands,
    label: 'Active Brands',
    url: -> { spree.admin_brands_path(status: 'active') },
    position: 10,
    active: -> { params[:status] == 'active' }

  brand_tabs.add :archived_brands,
    label: 'Archived Brands',
    url: -> { spree.admin_brands_path(status: 'archived') },
    position: 20,
    active: -> { params[:status] == 'archived' }
end
```

Then render the tabs in your view using the `render_tab_navigation` helper:

```erb app/views/spree/admin/brands/index.html.erb
<%= render_tab_navigation(:brand_tabs) %>
```

<Tip>
Always register custom contexts in your initializer before accessing them. Attempting to access an unregistered context will raise a `NoMethodError`.
</Tip>

### Listing All Contexts

You can list all registered navigation contexts:

```ruby
# Returns an array of context names (symbols)
Spree.admin.navigation.contexts
# => [:sidebar, :settings, :brand_tabs, :inventory_tabs]
```

### Checking If a Context Exists

```ruby
# Check if a context has been created
Spree.admin.navigation.context?(:brand_tabs)
# => true or false
```

## Creating Submenus

Add a parent item, then add child items using the `parent` option:

```ruby config/initializers/spree.rb
sidebar_nav.add :brands,
  label: :brands,
  url: :admin_brands_path,
  icon: 'award',
  position: 35,
  if: -> { can?(:manage, Spree::Brand) }

sidebar_nav.add :all_brands,
  label: 'All Brands',
  url: :admin_brands_path,
  position: 10,
  parent: :brands,
  active: -> { controller_name == 'brands' }

sidebar_nav.add :brand_categories,
  label: 'Brand Categories',
  url: :admin_brand_categories_path,
  position: 20,
  parent: :brands,
  active: -> { controller_name == 'brand_categories' },
  if: -> { can?(:manage, Spree::BrandCategory) }
```

<Info>
Parent items are automatically marked as active when any of their children are active. You don't need to manually define the `active` option for parent items.
</Info>

## Modifying Existing Navigation

### Finding Navigation Items

```ruby
sidebar_nav = Spree.admin.navigation.sidebar
products_nav = sidebar_nav.find(:products)
```

### Adding to Existing Submenus

Use the `parent` option to add an item to an existing submenu:

```ruby
sidebar_nav.add :brands,
  label: :brands,
  url: :admin_brands_path,
  position: 50,
  parent: :products,
  active: -> { controller_name == 'brands' },
  if: -> { can?(:manage, Spree::Brand) }
```

### Removing Navigation Items

```ruby
sidebar_nav.remove(:vendors)
```

### Updating Navigation Items

```ruby
sidebar_nav.update(:products, label: 'Catalog', icon: 'shopping-cart')
```

### Replacing Navigation Items

```ruby
sidebar_nav.replace(:products, label: 'Products', icon: 'package') do |products|
  # Define new submenu structure
end
```

### Moving Navigation Items

```ruby
# Move to specific position
sidebar_nav.move(:brands, position: 25)

# Move before another item
sidebar_nav.move(:brands, before: :products)

# Move after another item
sidebar_nav.move(:brands, after: :products)

# Move to first position
sidebar_nav.move(:brands, position: :first)

# Move to last position
sidebar_nav.move(:brands, position: :last)
```

## Advanced Examples

### Navigation with Dynamic Badge

```ruby
sidebar_nav.add :orders,
  label: :orders,
  url: :admin_orders_path,
  icon: 'inbox',
  position: 20,
  active: -> { controller_name == 'orders' },
  if: -> { can?(:manage, Spree::Order) },
  badge: -> {
    count = Spree::Order.ready_to_ship.count
    count if count.positive?
  },
  badge_class: 'badge-warning'
```

### Section Dividers

```ruby
sidebar_nav.add :settings_section,
  section_label: 'Settings',
  position: 90
```

### Dynamic URLs

```ruby
sidebar_nav.add :store_settings,
  label: :settings,
  url: -> { spree.edit_admin_store_path(section: 'general-settings') },
  icon: 'settings',
  position: 100
```

### Complex Conditional Display

```ruby
sidebar_nav.add :vendors,
  label: :vendors,
  url: 'https://spreecommerce.org/marketplace-ecommerce/',
  icon: 'heart-handshake',
  position: 35,
  if: -> { can?(:manage, current_store) && !defined?(SpreeEnterprise) },
  badge: 'Enterprise',
  tooltip: 'Multi-Vendor Marketplace is available in the Enterprise Edition',
  target: '_blank'
```

### Complex Active State Logic

```ruby
sidebar_nav.add :products,
  label: :products,
  url: :admin_products_path,
  icon: 'package',
  position: 30,
  active: -> {
    %w[products external_categories taxons taxonomies option_types option_values
       properties stock_items stock_transfers variants digital_assets].include?(controller_name)
  },
  if: -> { can?(:manage, Spree::Product) }
```

## Best Practices

<CardGroup cols={2}>
  <Card title="Authorization" icon="shield-check">
    Always use `if: -> { can?(...) }` to ensure users only see navigation items they have permission to access.
  </Card>

  <Card title="Translations" icon="language">
    Use symbols for labels (e.g., `label: :brands`) to support internationalization via `Spree.t`.
  </Card>

  <Card title="Active States" icon="pointer">
    Define clear active state logic using lambdas to highlight the current section properly.
  </Card>

  <Card title="Positioning" icon="list-numbers">
    Use consistent position intervals (e.g., 10, 20, 30) to leave room for future additions.
  </Card>
</CardGroup>

### Common Positioning Reference

Main sidebar navigation positions:

- Getting Started: 5
- Home: 10
- Orders: 20
- Returns: 25
- Products: 30
- Customers: 40
- Promotions: 50
- Reports: 60
- Storefront: 70
- Integrations: 80
- Settings Section: 90
- Settings: 100
- Admin Users: 110

### Troubleshooting

<AccordionGroup>
  <Accordion title="Navigation item not appearing">
    - Restart your server after modifying initializers
    - Check authorization: ensure `if: -> { can?(...) }` returns true
    - Verify the item isn't hidden by a parent's `if` condition
  </Accordion>

  <Accordion title="Active state not working">
    - Ensure `active:` lambda returns true/false
    - Check that controller_name or other conditions match correctly
    - For submenus, ensure parent uses same active logic as children
  </Accordion>

  <Accordion title="Badge not displaying">
    - Ensure the badge lambda returns a non-nil value
    - Check that the badge value is truthy (empty strings won't display)
    - For numeric badges, ensure the count is greater than 0
  </Accordion>
</AccordionGroup>

---

## Previous versions

<Warning>
The following documentation applies to Spree 5.1 and earlier. If you're using Spree 5.2+, please refer to the documentation above.
</Warning>

### How it works

The admin navigation system works through injection points defined throughout the sidebar. You can inject custom navigation items into these predefined locations, add new top-level menu items, or create nested submenus.

### Navigation Injection Points

The main navigation file is located at `admin/app/views/spree/admin/shared/sidebar/_store_nav.html.erb` and provides several injection points:

#### Available Injection Points

<AccordionGroup>
  <Accordion title="store_nav_partials">
    `store_nav_partials`

    Injects navigation items into the main sidebar navigation, after the Reports item and before the Storefront and Integrations sections.

    <Frame>
      <img src="/images/developer/admin/store_nav_partials.png" alt="Main navigation injection point" />
    </Frame>

    This is the primary injection point for adding custom top-level navigation items.
  </Accordion>

  <Accordion title="store_products_nav_partials">
    `store_products_nav_partials`

    Injects navigation items into the Products submenu, after the Properties item.

    Use this to add product-related navigation items that logically belong under the Products section.
  </Accordion>

  <Accordion title="store_orders_nav_partials">
    `store_orders_nav_partials`

    Injects navigation items into the Orders submenu, after the Draft Orders item.

    Use this to add order-related navigation items.
  </Accordion>

  <Accordion title="store_settings_nav_partials">
    `store_settings_nav_partials`

    Injects navigation items into the Settings section, after the Policies item.

    Use this when Settings mode is active to add configuration-related items.
  </Accordion>

  <Accordion title="settings_nav_partials">
    `settings_nav_partials`

    Injects navigation items at the end of the Settings section.

    Use this to add additional settings-related navigation items.
  </Accordion>
</AccordionGroup>

### Using the nav_item Helper

The `nav_item` helper method is provided by `Spree::Admin::NavigationHelper` and makes it easy to create properly formatted navigation items.

### Method Signature

```ruby
nav_item(label = nil, url, icon: nil, active: nil, data: {})
```

#### Parameters

<ParamField path="label" type="String">
  The text label for the navigation item. Can be HTML-safe content.
</ParamField>

<ParamField path="url" type="String">
  The URL the navigation item links to. Use the `spree.` route helper prefix.
</ParamField>

<ParamField path="icon" type="String" default="nil">
  Optional icon name from [Tabler Icons](https://tabler.io/icons). The icon will be displayed before the label.
</ParamField>

<ParamField path="active" type="Boolean" default="nil">
  Manually set whether the link should be marked as active. If not specified, it will be auto-detected based on the current URL.
</ParamField>

<ParamField path="data" type="Hash" default="{}">
  Additional data attributes to add to the link element.
</ParamField>

#### Basic Usage

```erb
<%= nav_item(Spree.t(:custom_section), spree.admin_custom_path, icon: 'star') %>
```

#### With Active State

```erb
<%= nav_item(
  Spree.t(:inventory),
  spree.admin_inventory_path,
  icon: 'boxes',
  active: controller_name == 'inventory'
) %>
```

#### With Block Content

```erb
<%= nav_item(nil, spree.admin_dashboard_path, icon: 'home') do %>
  <%= icon 'home' %>
  <%= Spree.t(:dashboard) %>
  <span class="badge ml-auto">New</span>
<% end %>
```

### Adding a Simple Navigation Item

Let's add a new "Inventory" navigation item to the main sidebar.

#### Step 1: Create the Partial

```bash
mkdir -p app/views/spree/admin/shared
touch app/views/spree/admin/shared/_inventory_nav.html.erb
```

#### Step 2: Add Navigation Code

```erb app/views/spree/admin/shared/_inventory_nav.html.erb
<% if can?(:manage, Spree::Inventory) %>
  <%= nav_item(
    Spree.t(:inventory),
    spree.admin_inventory_index_path,
    icon: 'boxes',
    active: controller_name == 'inventory'
  ) %>
<% end %>
```

<Tip>
Always wrap your navigation items with authorization checks using `can?()` to ensure users only see menu items they have permission to access.
</Tip>

#### Step 3: Register the Partial

Add this to your `config/initializers/spree.rb`:

<Tabs>
  <Tab title="Spree 5.2+">
    ```ruby config/initializers/spree.rb
    Spree.admin.navigation.store << 'spree/admin/shared/inventory_nav'
    ```
  </Tab>
  <Tab title="Spree 5.1 and below">
    ```ruby config/initializers/spree.rb
    Rails.application.config.spree_admin.store_nav_partials << 'spree/admin/shared/inventory_nav'
    ```
  </Tab>
</Tabs>

#### Step 4: Add Translations

In your `config/locales/en.yml`:

```yaml config/locales/en.yml
en:
  spree:
    inventory: "Inventory"
```

#### Step 5: Restart Your Server

Restart your web server to load the initializer changes. The navigation item should now appear in the sidebar.

### Creating Nested Navigation (Submenus)

To create a navigation item with a submenu, you need to use the `nav-submenu` class and manage the visibility based on the active state.

#### Example: Adding a Nested Menu

```erb
<% inventory_active = %w[inventory warehouses stock_movements].include?(controller_name) %>

<% if can?(:manage, Spree::Inventory) %>
  <%= nav_item(
    Spree.t(:inventory),
    spree.admin_inventory_index_path,
    icon: 'boxes',
    active: inventory_active
  ) %>

  <ul class="nav-submenu <% unless inventory_active %>d-none<% end %>">
    <% if can?(:manage, Spree::Warehouse) %>
      <%= nav_item(
        Spree.t(:warehouses),
        spree.admin_warehouses_path,
        active: controller_name == 'warehouses'
      ) %>
    <% end %>

    <% if can?(:manage, Spree::StockMovement) %>
      <%= nav_item(
        Spree.t(:stock_movements),
        spree.admin_stock_movements_path,
        active: controller_name == 'stock_movements'
      ) %>
    <% end %>

    <%= render_admin_partials(:store_inventory_nav_partials) %>
  </ul>
<% end %>
```

#### Key Points for Submenus

1. **Active State Variable**: Define a variable to track when any item in the menu group is active:
   ```erb
   <% inventory_active = %w[inventory warehouses stock_movements].include?(controller_name) %>
   ```

2. **Parent Navigation Item**: Use the active state variable for the parent item:
   ```erb
   <%= nav_item(..., active: inventory_active) %>
   ```

3. **Submenu Container**: Use the `nav-submenu` class and conditionally add `d-none` to hide when inactive:
   ```erb
   <ul class="nav-submenu <% unless inventory_active %>d-none<% end %>">
   ```

4. **Child Items**: Add child navigation items within the submenu:
   ```erb
   <%= nav_item(Spree.t(:child_item), spree.admin_child_path) %>
   ```

5. **Nested Injection Point** (Optional): Add an injection point within the submenu for further extensibility:
   ```erb
   <%= render_admin_partials(:store_inventory_nav_partials) %>
   ```

### Advanced Examples

#### Navigation with Badge

```erb
<%= nav_item(nil, spree.admin_orders_path, icon: 'inbox', active: orders_active) do %>
  <%= icon 'inbox' %>
  <%= Spree.t(:orders) %>
  <span class="badge ml-auto"><%= pending_orders_count %></span>
<% end %>
```

#### Navigation with Complex Active Logic

```erb
<% products_active = %w[products external_categories taxons taxonomies option_types option_values properties stock_items stock_transfers].include?(controller_name) || request.path.include?('products') %>

<%= nav_item(
  Spree.t(:products),
  spree.admin_products_path,
  icon: 'package',
  active: products_active
) %>
```

#### Extending Existing Submenus

To add an item to an existing submenu (e.g., Products), use the appropriate injection point:

**Create:** `app/views/spree/admin/shared/_custom_products_nav.html.erb`

```erb
<% if can?(:manage, Spree::CustomProductFeature) %>
  <%= nav_item(
    Spree.t(:custom_feature),
    spree.admin_custom_product_feature_path,
    active: controller_name == 'custom_product_features'
  ) %>
<% end %>
```

**Register in** `config/initializers/spree.rb`:

<Tabs>
  <Tab title="Spree 5.2+">
    ```ruby config/initializers/spree.rb
    Spree.admin.navigation.store_products << 'spree/admin/shared/custom_products_nav'
    ```
  </Tab>
  <Tab title="Spree 5.1 and below">
    ```ruby config/initializers/spree.rb
    Rails.application.config.spree_admin.store_products_nav_partials << 'spree/admin/shared/custom_products_nav'
    ```
  </Tab>
</Tabs>

### Navigation in Settings Mode

When the admin is in "Settings mode" (the dedicated settings view), use the `settings_nav_partials` injection point:

```erb
<% if settings_active? %>
  <!-- Settings mode is active -->
  <%= nav_item(Spree.t(:custom_settings), spree.edit_admin_store_path(section: 'custom-settings'), icon: 'adjustments') %>
<% end %>
```

Register with:

<Tabs>
  <Tab title="Spree 5.2+">
    ```ruby config/initializers/spree.rb
    Spree.admin.navigation.settings << 'spree/admin/shared/custom_settings_nav'
    ```
  </Tab>
  <Tab title="Spree 5.1 and below">
    ```ruby config/initializers/spree.rb
    Rails.application.config.spree_admin.settings_nav_partials << 'spree/admin/shared/custom_settings_nav'
    ```
  </Tab>
</Tabs>

### Best Practices

<CardGroup cols={2}>
  <Card title="Authorization" icon="shield-check">
    Always use `can?()` checks to ensure users only see navigation items they have permission to access.
  </Card>

  <Card title="Translations" icon="language">
    Use `Spree.t()` for all navigation labels to support internationalization.
  </Card>

  <Card title="Icons" icon="icons">
    Use consistent icons from [Tabler Icons](https://tabler.io/icons) that match Spree's design language.
  </Card>

  <Card title="Active States" icon="pointer">
    Define clear active state logic to highlight the current section in the navigation.
  </Card>

  <Card title="Route Helpers" icon="route">
    Always use `spree.` prefixed route helpers to reference admin routes correctly.
  </Card>

  <Card title="Injection Points" icon="puzzle">
    Add your own injection points in submenus to allow further extensions by other developers.
  </Card>
</CardGroup>

### Common Patterns

#### Multiple Controller Check

```erb
<% active = %w[orders shipments payments].include?(controller_name) %>
```

#### Path-based Check

```erb
<% active = request.path.include?('products') %>
```

#### Controller and Action Check

```erb
<% active = controller_name == 'dashboard' && action_name == 'show' %>
```

#### Parameters-based Check

```erb
<% active = params[:section] == 'general-settings' %>
```

### Troubleshooting

<AccordionGroup>
  <Accordion title="Navigation item not appearing">
    - Ensure you've restarted your web server after adding the initializer
    - Check that the authorization check (`can?()`) is passing
    - Verify the partial path is correct (without `_` prefix and `.html.erb` suffix)
    - Check that the route helper exists and is correct
  </Accordion>

  <Accordion title="Icon not displaying">
    - Verify the icon name exists in [Tabler Icons](https://tabler.io/icons)
    - Check that you're using the correct parameter name: `icon:` not `icon_name:`
    - Ensure the icon name is a string, e.g., `icon: 'boxes'`
  </Accordion>

  <Accordion title="Submenu not showing/hiding properly">
    - Ensure the active state variable includes all relevant controller names
    - Check that the `nav-submenu` class is applied to the `<ul>` element
    - Verify the `d-none` class is conditionally added when not active
    - Make sure the parent nav item uses the same active state variable
  </Accordion>

  <Accordion title="Translation missing">
    - Add the translation key to your locale file
    - Ensure the locale file is in the correct location
    - Restart your server after adding translations
    - Check for typos in the translation key
  </Accordion>
</AccordionGroup>

### Related Documentation

- [Extending Admin UI](/developer/admin/extending-ui) - Learn about other UI injection points
- [Helper Methods](/developer/admin/helper-methods) - Explore other admin helper methods
- [Permissions](/developer/customization/permissions) - Understand the authorization system
